#include "md5_mesh_builder.h"
#include "index_buffer.h"
#include "vertex_buffer.h"
#include "renderer.h"
#include "md5_anim_parser.h"
#include "file_util.h"
#include "matrix.h"
#include "string_util.h"
#include "animation.h"
#include "skeleton.h"
#include <iostream>
#include <algorithm>

using namespace krryn::math;
using namespace krryn::data;
using namespace krryn::view;
using namespace krryn::core;
using namespace krryn::animation;

md5_mesh_builder::md5_mesh_builder(const std::string &a_Directory) 
	: m_Parser(make_mesh_filename(a_Directory)){


	try{
		m_Parser.parse();
		auto l_Animations = files_in_dir(a_Directory, "md5anim");

		std::string ext = ".md5anim";

		for(auto i = l_Animations.begin(); i != l_Animations.end(); i++){
			std::string l_Name = i->m_Filename.substr(0, i->m_Filename.size() - ext.size());

			std::cout << "Parsing animation: " <<  i->m_Filename << " " << l_Name << std::endl;

			auto l_AnimParser = new md5_anim_parser(a_Directory + "/" + i->m_Filename, l_Name);
			l_AnimParser->parse();
			m_AnimationParsers.push_back(l_AnimParser);
		}
	}catch(md5_parse_error &a_Error){
		std::cout << a_Error.what() << std::endl;
	}
}

md5_mesh_builder::~md5_mesh_builder(){
	for(auto i = m_AnimationParsers.begin(); i != m_AnimationParsers.end(); i++){
		delete *i;
	}
}

model *md5_mesh_builder::build_model(renderer &a_Renderer){
	// TODO: build vertex format
	// TODO: let meshes create render_operations
	model *l_Result = new model();

	md5_model &l_Model = m_Parser.get_model();
	for(int i = 0; i < l_Model.m_NumMeshes; i++){
		md5_mesh &l_Md5Mesh = l_Model.m_Meshes[i];

		// TODO: We might want to share the VB with all meshes in a model
		view::mesh *l_Mesh = new view::mesh(
			build_index_buffer(a_Renderer, l_Md5Mesh),
			build_vertex_buffer(a_Renderer, l_Md5Mesh, l_Model.m_BindingPose),
			build_material(a_Renderer, l_Md5Mesh));

		l_Result->add_mesh(l_Mesh);
	}

	l_Result->set_bindpose(build_skeleton(l_Model.m_NumJoints, l_Model.m_BindingPose));

	for(auto i = m_AnimationParsers.begin(); i != m_AnimationParsers.end(); i++){
		md5_animation &l_Anim = (*i)->get_animation();

		l_Result->add_animation(
			(*i)->get_name(), 
			build_animation(
				l_Result,
				l_Anim.m_FrameRate, 
				l_Anim.m_NumFrames, 
				l_Anim.m_NumJoints, 
				l_Anim.m_SkeletalFrames
			)
		);
	}

	return l_Result;
}

animation *md5_mesh_builder::build_animation(model *a_Model,int a_FrameRate, int a_NumFrames, int a_NumJoints, md5_joint **a_Joints){
	animation::animation *l_Animation = new animation::animation(a_Model, a_FrameRate, a_NumFrames);

	for(int j = 0; j < a_NumFrames; j++){
		l_Animation->set_skeleton(j, build_skeleton(a_NumJoints, a_Joints[j]));
	}

	return l_Animation;
}

skeleton *md5_mesh_builder::build_skeleton(int a_NumJoints, md5_joint *a_Joints){
	skeleton *l_Skeleton = new skeleton(a_NumJoints);
	quaternionf a_Rot = m_CoordinateCorrection.as_quaternion();

	for(int k = 0; k < a_NumJoints; k++){
		l_Skeleton->set_joint_position(k, m_CoordinateCorrection.transform(a_Joints[k].m_Position));
		l_Skeleton->set_joint_parent(k,   a_Joints[k].m_Parent == -1 ? skeleton::g_InvalidParent : a_Joints[k].m_Parent);
		l_Skeleton->set_joint_rotation(k, a_Rot * a_Joints[k].m_Rotation);
	}

	return l_Skeleton;
}

std::string md5_mesh_builder::make_mesh_filename(const std::string &a_Path){
	std::vector<std::string> l_Parts = explode(a_Path, "/");
	return a_Path + "/" + l_Parts[l_Parts.size() - 1] + ".md5mesh";
}

refc_ptr<material> md5_mesh_builder::build_material(renderer &a_Renderer, md5_mesh &a_Mesh){
	refc_ptr<material> l_Material(new material);

	if(!ends_with(a_Mesh.m_Shader, "_collision")){
		try{
			l_Material->m_Diffuse = a_Renderer.get_texture_catalog()->get_texture("data/" + a_Mesh.m_Shader + "_d.tga");
			l_Material->m_Normals = a_Renderer.get_texture_catalog()->get_texture("data/" + a_Mesh.m_Shader + "_local.tga");
			l_Material->m_Specular = a_Renderer.get_texture_catalog()->get_texture("data/" + a_Mesh.m_Shader + "_s.tga");
		}catch(...){}
	}

	return l_Material;
}

refc_ptr<index_buffer> md5_mesh_builder::build_index_buffer(renderer &a_Renderer, md5_mesh &a_Mesh){
	std::vector<unsigned int> l_Indices;

	int l_Winding[] = {0, 2, 1};

	for(int i = 0; i < a_Mesh.m_NumTriangles; i++){
		for(int j = 0; j < 3; j++){
			l_Indices.push_back(a_Mesh.m_Triangles[i].m_Vertices[l_Winding[j]]);
		}
	}

	refc_ptr<index_buffer> l_IndexBuffer = a_Renderer.create_index_buffer(
		a_Mesh.m_NumTriangles * 3, index_32bit);

	l_IndexBuffer->write_data(0, sizeof(unsigned int) * a_Mesh.m_NumTriangles * 3, &l_Indices[0]);

	return l_IndexBuffer;
}

refc_ptr<vertex_buffer> md5_mesh_builder::build_vertex_buffer(
	renderer &a_Renderer, 
	md5_mesh &a_Mesh, 
	md5_joint *a_Skeleton)
{
	// http://www.doom3world.org/phpbb2/viewtopic.php?p=100174#100020
	std::vector<vector3f> l_BindPosePositions(a_Mesh.m_NumVertices);
	std::vector<vector3f> l_BindPoseNormals(a_Mesh.m_NumVertices);
		
	// Code assumes vector3f is default constructed to zero
	compute_bind_pose_positions(a_Mesh, a_Skeleton, l_BindPosePositions);
	compute_bind_pose_normals(a_Mesh, l_BindPosePositions, l_BindPoseNormals);

	refc_ptr<vertex_buffer> l_VertexBuffer = 
		a_Renderer.create_vertex_buffer(8 * sizeof(float), a_Mesh.m_NumVertices);

	float *vb = (float*)l_VertexBuffer->lock(hardware_buffer::write);

	for(int i = 0; i < a_Mesh.m_NumVertices; i++){
		vector3f l_Position = l_BindPosePositions[i];
		vector3f l_Normal = l_BindPoseNormals[i].normalize();

		*vb++ = l_Position.m_X; *vb++ = l_Position.m_Y; *vb++ = l_Position.m_Z;
		*vb++ = l_Normal.m_X;   *vb++ = l_Normal.m_Y;   *vb++ = l_Normal.m_Z;

		*vb++ = a_Mesh.m_Vertices[i].m_UV.m_U;
		*vb++ = a_Mesh.m_Vertices[i].m_UV.m_V;
	}

	l_VertexBuffer->unlock();
	return l_VertexBuffer;
}

void md5_mesh_builder::compute_bind_pose_positions(
	md5_mesh &a_Mesh, 
	md5_joint *a_Skeleton, 
	std::vector<vector3f> &a_BindPosePositions)
{
	// correct for the krryn coordinate system
	for(int i = 0; i < a_Mesh.m_NumVertices; i++){
		vector3f l_Position = vector3f::zero();

		for(int j = 0; j < a_Mesh.m_Vertices[i].m_WeightCount; j++){
			int l_Offset = a_Mesh.m_Vertices[i].m_WeightStart;

			md5_weight &l_Weight = a_Mesh.m_Weights[l_Offset + j];
			md5_joint &l_Joint   = a_Skeleton[l_Weight.m_Joint];

			vector3f l_Temp = 
				l_Joint.m_Rotation.rotate(l_Weight.m_Position);

			l_Position += 
				(l_Joint.m_Position + l_Temp) * l_Weight.m_Bias;
		}

		a_BindPosePositions[i] = m_CoordinateCorrection.transform(l_Position);
	}
}

void md5_mesh_builder::compute_bind_pose_normals(md5_mesh &a_Mesh, std::vector<vector3f> &a_BindPosePositions, std::vector<vector3f> &a_BindPoseNormals){
	for(int i = 0; i < a_Mesh.m_NumTriangles; i++){
		md5_triangle &l_Tri = a_Mesh.m_Triangles[i];

		vector3f l_TriNormal = compute_triangle_normal(
			a_BindPosePositions[l_Tri.m_Vertices[0]],
			a_BindPosePositions[l_Tri.m_Vertices[1]],
			a_BindPosePositions[l_Tri.m_Vertices[2]]);

		for(int j = 0; j < 3; j++){
			a_BindPoseNormals[l_Tri.m_Vertices[j]] += l_TriNormal;
		}
	}
}

vector3f md5_mesh_builder::compute_triangle_normal(
	vector3f a_P1, 
	vector3f a_P2, 
	vector3f a_P3)
{
	vector3f l_Normal, l_Edge1, l_Edge2;

	l_Edge1 = a_P2 - a_P1;
	l_Edge2 = a_P3 - a_P1;

	return -l_Edge1.cross(l_Edge2).normalize();
}